<html><head> 
<script type="text/javascript">
setPublic(modellinker, "com.cordys.opensource.crom");

importType("cas.xforms.runtimelibrary.CompositeControl");
inherit(modellinker, CompositeControl);

function modellinker()
{
}

/**
 * Use this function to force an update of the active object in the parent model.
 *  This will invoke the "edit" method on the active object in the parent model,
 *  causing the creation of old/new tuple if the object has not been changed yet.
 *  Effectively this triggers the modellinker logic that is normally invoked if
 *  an insert/update/delete event happens on the child model.
 */
modellinker.prototype.updateParent = function ()
{
	handleChange({type:"onAPI-updateParent"}, this.linker);
}

/**
 * Use this function to clear the data in the child model and overwrite it
 *  freshly with the current active business object from the parent model
 */
modellinker.prototype.refreshChild = function ()
{
	loadData({type:"onAPI-refreshChild"}, this.linker);
}

/**
 * Use this function to disable the modellinker.
 */
 modellinker.prototype.disable = function ()
{
	this.linker.disabled = true;
	unregisterListeners(this.linker);
}

/**
 * Use this function to enable the modellinker.
 */
 modellinker.prototype.enable = function ()
{
	this.linker.disabled = false;
	registerListeners(this.linker);
}

modellinker.attachType = function ( control )
{
//	if (!confirm("Press cancel to debug")) debugger;

	// TODO: set properties on control, so we can clean it up properly.

	var strParentModel = control.getAttribute("parentModel");
	var strChildModel = control.getAttribute("model");
	var xpath = control.getAttribute("xpath");
	var xpathParent = control.getAttribute("xpathParent");
	var namespaces = control.getAttribute("namespaces");
	var instanceNamespace = control.getAttribute("instanceNamespace");
	
	var parentModel = control.ownerDocument.defaultView[strParentModel];
	if (!parentModel)
	{
		parentModel = control.ownerDocument.defaultView.eval(strParentModel);
	}
	var childModel = control.ownerDocument.defaultView[strChildModel];
	if (childModel.automatic)
	{
		alert("Model "+childModel.id+" is automatic, but it should not be!");
	}
	if (!parentModel)
	{
		if (!strParentModel)
		{
			alert("Parent model has not been defined for the model linker with name "+control.id);
		}
		else
		{
			alert("Parent model '"+strParentModel+"' cannot be found.");
		}
		return;
	}
	
	var context = {parentModel:parentModel, childModel:childModel, control:control };
	context.toString = function()
	{
		return "Linker from "+this.parentModel.id+" to "+this.childModel.id;
	}
	control.linker = context;
	
	if (xpath)
	{
		context.xpath = xpath;
		if (!xpathParent)
		{
			alert("A specific XPath expression is given in "+control.id+", but the xpath to determine the parent node has not been set.");
			return;
		}
		context.xpathParent = xpathParent;
		if (!namespaces)
		{
			alert("A specific XPath expression is given in "+control.id+", but namespace prefixes have not been set.");
			return;
		}
		context.namespaces = eval("("+namespaces+")");
		if (!instanceNamespace)
		{
			alert("A specific XPath expression is given in "+control.id+", but the instance namespace has not been set.");
			return;
		}
		context.instanceNamespace = instanceNamespace;
	}
	else
	{
		var objectName = childModel.instance.businessObjectName;
		// Apparently sometimes the objectname is prefixed ...
		if (objectName.indexOf("tns:")==0) objectName = objectName.substring(4);
		var prefix = "tns";  // Not sure if it is smart to use tns: ourselves as well ?!
		context.xpath = prefix+":"+objectName;
		context.xpathParent = ".";
		context.namespaces = new Object();
		if (childModel.instance.instanceNamespace)
		{
			context.namespaces[prefix] = childModel.instance.instanceNamespace;
		}
		else if (childModel.XPathMetaInformation.xmlns)
		{
			context.namespaces[prefix] = childModel.XPathMetaInformation.xmlns;
		}
		else
		{
			alert("Could not determine namespace for object "+objectName+" in model "+childModel.id+". Please configure namespaces in model linker "+control.id);
			return;
		}
		context.instanceNamespace = context.namespaces[prefix];
	}
	
	registerListeners(context);
}

function registerListeners(context)
{
	var parentModel = context.parentModel;
	var childModel = context.childModel;

	// Check if the parent has been initialized
	if (!parentModel.__crom_linkers)
	{
		// Create the links to the child linkers
		parentModel.__crom_linkers = new Array();

		// Replace the synchronize method on the parent so that we can handle the synchronize ourselves
		parentModel.__xformsSynchronizeMethod = parentModel.synchronize;
		parentModel.synchronize = beforeSynchronizeParent;
		
		// Make ourselves a dummy view on the parent model.
		//  That way, the parent model will automatically receive the
		//  onactivebusinessobject event after datacompleted, even if no other view is registered.
		parentModel.getView(1, context.control, context.control.ownerDocument.defaultView);
	}
	parentModel.__crom_linkers.push(context);
	
	parentModel.addListener("xforms-onactivebusinessobject", handleOnActiveBusinessObject, context);
	
	childModel.parentLinker = context;

	childModel.addListener("xforms-onafterinsert", handleChange, context);
	childModel.addListener("xforms-onafterdelete", handleChange, context);
	childModel.addListener("xforms-onafterchange", handleChange, context);
	childModel.addListener("xforms-delete", setDeletionMode, context);
	childModel.addListener("xforms-ondatacompleted", dataCompleted, context);
	childModel.addListener("xforms-onbeforesynchronize", cancelSynchronize, context);
	
	context.control.ownerDocument.defaultView.application.addListener("onapplicationselect", selectApplication, context);
	context.deletedTuples = new Array();

	// This event can probably be removed. We have not seen it NOT working without this code. It has been there since the beginning, and was
	//  probably introduced back then because of lack of knowledge of XForms.
	loadData({type:"no event"}, context);
}

function unregisterListeners(context)
{
	var parentModel = context.parentModel;
	var childModel = context.childModel;

	parentModel.removeListener("xforms-onactivebusinessobject", handleOnActiveBusinessObject, context);
	

	childModel.removeListener("xforms-onafterinsert", handleChange, context);
	childModel.removeListener("xforms-onafterdelete", handleChange, context);
	childModel.removeListener("xforms-onafterchange", handleChange, context);
	childModel.removeListener("xforms-delete", setDeletionMode, context);
	childModel.removeListener("xforms-ondatacompleted", dataCompleted, context);
	childModel.removeListener("xforms-onbeforesynchronize", cancelSynchronize, context);
	
	context.control.ownerDocument.defaultView.application.removeListener("onapplicationselect", selectApplication, context);
}

function selectApplication(eventObject, context)
{
//	alert("This is selectApplication, log: "+logBuffer);
	setTimeout(new function() {
		context.childModel.refreshAllViews();
		}, 0);

	log(context, "Application select: refreshed all views");
}

function dataCompleted(eventObject, context)
{
//	if (!confirm("Setting deletion mode during "+eventObject.type+". Press cancel to debug")) debugger;
	log(context, context.childModel.id+".dataCompleted");
}

function setDeletionMode(eventObject, context)
{
	// This event listener is active upon deletion of objects. XForms immediately synchronizes a model
	//  when an object is deleted. Here we track the object to be deleted in the context. 
	//  The reason for this is the "cancelSynchronize" listener. This listener will cancel the synchronize event,
	//  and subsequently the delete action will be canceled by XForms. However, this deletion should not be canceled.
	//  Hence we track the object and then do the delete ourselves during cancelSynchronize.
	var tuple = eventObject.tuple;
	if (tuple.parentNode)
	{
		log(context, "Entering deletion mode");
		// Only add it if there is a paretn node. Not sure why, as
		//  it wasn't documented at that time. Seem to remember that
		//  it was because of newly inserted tuples that are immediately
		//  deleted (which is a typical use case when a table has the
		//   auto-insert property enabled)
		context.deletedTuples.push(eventObject.tuple);
	}
}
function cancelSynchronize(eventObject, context)
{
	// Never synchronize child models. Default XForms behavior synchronizes a child model whenever it has changes and the
	//  parent model gets a different active business object. That would create a roundtrip to the backend which needs to be
	//  avoided, as the whole purpose of modellinker is to use the child model only to represent data and not to do roundtrips.
	//  The parent model is responsible for the roundtrip. 
	//  So cancelling the synchronize here.
	eventObject.returnValue = false;
	
	// Now check if the synchronize-attempt has happened because of deleted tuples.
	//  If that is the case, still the synchronize attempt will be canceled, however, XForms will then also cancel the
	//  delete event. But that is not what we want. Hence we will delete these objects ourselves and also manually invoke
	//  the change handler.
	var deletedTuples = context.deletedTuples;
	if (deletedTuples.length>0)
	{
		log(context, "Cancel synchronize: deleting "+deletedTuples.length+" tuples");
		// Delete all those objects selected for deletion
		for (var i=0; i<deletedTuples.length; i++)
		{
			var tuple = deletedTuples[i];
			tuple.parentNode.removeChild(tuple);
		}
		// Handle the change (i.e., propagate it into the parent model).
		handleChange(eventObject, context);
		
		// Statement added for Chrome. This is related to some dependency within XForms' view.htm:
		//  a view keeps track of it's tuples in the view.tuples XML selection.
		//  This selection is an XPath selection, and in Internet Explorer, this selection is always
		//  kept up-to-date. So, in the above lines we remove the deleted tuples from the XML data,
		//  and in IE, the selection is immediately updated.
		// However, in other browsers (at least Chrome, and probably also others) we have noticed
		//  that the selection is not immediately updated. Hence we invoke the loadData method,
		//  which in turn somehow takes care that the view is properly updated.
		if (!system.isIE)
		{
			loadData(eventObject, context);
		}
		
		// ... and clear the registry, so that the same object is not deleted again and again!
		context.deletedTuples = new Array();
	}
}

/**
 * This method is an overload of the XForms synchronize() method.
 *  Unfortunately this is required, since sometimes the synchronize method is invoked
 *  without having captured all changes in all child models. So before the actual
 *  XForms' synchronize method is invoked, we will first invoke 'handleChange' to get
 *  the latest and greatest data from the child models.
 */
function beforeSynchronizeParent()
{
	log(context, "Handling "+this.id+".synchronize()");
	
	// Check to see what the reason is why XForms invokes the synchronize method
	//  One of the reasons is during the deletion of objects from a view.
	//  In that case, the onbeforesynchronize event will invoke the cancelSynchronize
	//  method which will take care of handling changes.
	//  In all other cases we have to do last minute capturing of the state of the children
	//  because otherwise the onchange, oninsert, ondelete events will not be captured
	//  on the children.
	if (this.parentLinker)
	{
		if (this.parentLinker.deletedTuples.length==0) // I.e., synchronize() is NOT invoked because of a delete of object from view
		{
			for (var context in this.__crom_linkers)
			{
				// do some last minute refresh of the data in the parent (based on changes in children).
				handleChange({type:"onsynchronizeparent"}, this.__crom_linkers[context]);
			}
		}
		else
		{
			log(context, "There are deleted tuples, synchronize will not be propagated to child models");
		}
	}
	
	// Make an array of our arguments so that we can invoke the real synchronize method
	var args = [];
	for(var i = 0; i < arguments.length; i++)
	{
		args.push(arguments[i]);
	}
	// Now invoke the real synchronize method on the parent model, and pass in all our arguments.
    this.__xformsSynchronizeMethod.apply(this, args);
}

var dataDocument;

/**
 * This method propagates the changes in the child model back into the parent model.
 */
function handleChange(eventObject, context)
{
	if (context.disabled)
	{
		return;
	}
	indent++;
// 	if (!confirm("Handle change on "+context.childModel.id+",  during "+eventObject.type)) debugger;

	// get the node where all updates should be bundled
	var mainUpdateNode = context.parentModel.edit(context.activeBusinessObject);//context.activeBusinessObject;
	
	if (!mainUpdateNode) // This can be the case if synchronize is invoked and no data is yet available on the model.
	{
		return;
	}

	// Get all data from the child.
	//  This data can have two formats. It can be either 
	//   1. <dataset><data><Response xmlns="instanceNamespace"><tuple> ....
	//    or
	//   2. <dataset>data><clientcreated><tuple xmlns="instanceNamespace"> ....
	//  Number 1. will occur in case of existing data. Number 2. will occur in case of client-side created objects, i.e., newly inserted objects.
	//   The newly created objects have a somewhat different XML structure, generated by XForms.
	//   The <Response> tag in the number 1. objects is created in our own loadData method.
	//  Also, inside the tuple we can have both <new> and <old> tags. Here we have 3 different options.
	//   - only <new>; this means it is newly inserted.
	//   - only <old>; this means no change has happened on the object.
	//   - both <old> and <new>; this means a change has happened to this specific object
	//  Since the "real" old is inside the parent model, we don't have to bother about preserving both old and new. 
	//   Instead, we can suffice with selecting just the object inside <new> if available. And only for those objects
	//   that have not changed, we can select the object inside <old>.
	//   Hence the below xpath expression: select <new>'s object if available, and select only those <old>'s objects for
	//	 which we do NOT have have <new> tag inside the <tuple>
	var prefixInstanceNamespace = {"tns":context.instanceNamespace};
	var xpathSelectObjects = "dataset/*/*/tns:tuple[tns:new]/tns:new/* | dataset/*/*/tns:tuple[not(tns:new)]/tns:old/*";
	var allTuples = cordys.selectXMLNodes(context.childModel.getData(), xpathSelectObjects, prefixInstanceNamespace);
	if (allTuples.length==0)
	{
		xpathSelectObjects = "dataset/*/*/tuple[new]/new/* | dataset/*/*/tuple[not(new)]/old/*";
		allTuples = cordys.selectXMLNodes(context.childModel.getData(), xpathSelectObjects, prefixInstanceNamespace);
	}
	
	
//	var xmlInChild = cordys.getXML(context.childModel.getData());
	log(context, "Handle change during "+eventObject.type+" in model "+context.childModel.id);//+". Data:\n"+xmlInChild);

	// Replace the node at the right place in the mainUpdateNode
	var oldSubNodes = cordys.selectXMLNodes(mainUpdateNode, context.xpath, context.namespaces);
	
	var subParentNode = cordys.selectXMLNode(mainUpdateNode, context.xpathParent, context.namespaces);
	if (!subParentNode)
	{
		var oncreateParentEvent = { activeParentObject: mainUpdateNode, xpath: context.xpathParent, namespaces: context.namespaces };
		context.control.raiseEvent("oncreateparent", oncreateParentEvent);
		// And now select the parent again. Now it is expected to have been created.
		subParentNode = cordys.selectXMLNode(mainUpdateNode, context.xpathParent, context.namespaces);
		if (!subParentNode)
		{
			// This is an error situation.
			if (allTuples.length==0 && oldSubNodes.length==0)
			{
				// No need to do anything. Or perhaps 
			}
			else
			{
				alert("Error: a parent node is not available for model "+context.childModel.id+", as the xpath expression "+context.xpathParent+" did not yield any result");
			}
		}
	}
	
	// remove all old nodes
	for ( var n = 0; n < oldSubNodes.length; n++)
	{
		subParentNode.removeChild(oldSubNodes[n]);
	}
	// add all new nodes
	for ( var n = 0; n < allTuples.length; n++)
	{
		var tuple = allTuples[n].cloneNode(true);
		cordys.appendXMLNode(tuple, subParentNode);
	}
	
	// Inform that the parent has been updated with the changed children
	context.control.raiseEvent("onafterupdateparent");
	
	if (context.parentModel.parentLinker)
	{
		log(context, "Propagating change to parent");
		handleChange(eventObject, context.parentModel.parentLinker);
	}
	
	log(context, "Added "+allTuples.length+" objects in model "+context.parentModel.id);// data: "+cordys.getXML(context.activeBusinessObject));
	indent--;
}

function refreshData(eventObject, context)
{
	log(context, "Invoking our own refresh");
}

/**
 * Listener dealing with OnActiveBusinessObject event. Simply invoke loadData
 */
function handleOnActiveBusinessObject(eventObject, context)
{
	if (context.disabled)
	{
		return;
	}
	
	loadData(eventObject, context);
}

/**
 * Load data from the parent model into the child model.
 * Given the xpath inside the context object, it will search for the data in the parent, add the tuple old structure and store it in a
 * separate SOAP document, which can be put into the child model
 * 
 * @param context
 * @returns
 */
function loadData(eventObject, context)
{
	// More defensive: also check it here, and not just while handling "onactivebusinessobject"
	//  reason: in some use cases apparently modellinker is disabled after the onactivebusinessobject event
	//  and before the loadData is handled.
	if (context.disabled)
	{
		return;
	}

indent++;
log(context, "Handling "+context.parentModel.id+"."+eventObject.type);
//	if (!confirm("Handling "+context.parentModel.id+"."+eventObject.type+" in child model "+context.childModel.id)) debugger;

	var parentModel = context.parentModel;
	var childModel = context.childModel;
	var xpath = context.xpath;
	
	var currentBusinessObject = parentModel ? parentModel.getActiveBusinessObject() : 0;
	var state = currentBusinessObject ? currentBusinessObject.getState() : "";
	
	if (currentBusinessObject && state!="" && state!="delete") // Only load when an active object is available on the parent.
	{
		if (!dataDocument)
		{
			dataDocument = cordys.getXMLDocument(); // Reuse the document for performance reason
		}
		
		var resultNode = cordys.loadXMLDocument("<SOAP:Envelope xmlns:SOAP='http://schemas.xmlsoap.org/soap/envelope/'><SOAP:Body><Response xmlns=\""+context.instanceNamespace+"\"/></SOAP:Body></SOAP:Envelope>");
		var tupleNode = cordys.loadXMLDocument("<tuple xmlns='"+context.instanceNamespace+"'><old/></tuple>", dataDocument);

		// Select the content from the parent
		var dataNodes = cordys.selectXMLNodes(currentBusinessObject.businessObject, xpath, context.namespaces);
		for (var i = 0; i < dataNodes.length; i++)
		{
			var dataNode = dataNodes[i];
			if (dataNode)
			{
				dataNode = dataNode.cloneNode(true);
				var newTuple = tupleNode.documentElement.cloneNode(true);
				cordys.appendXMLNode(dataNode, newTuple.firstChild);
				cordys.appendXMLNode(newTuple, resultNode.documentElement.firstChild.firstChild);
			}
		}

		//var xmlInChild = cordys.getXML(resultNode);

		// Clear the current data in the child model, and overwrite it with the new data
		log(context, "Clearing data in "+childModel.id);
		childModel.clear();

		// Keep a pointer to the active business object. (not sure if this is necessary)
		context.activeBusinessObject = currentBusinessObject.businessObject;
		
		var views = childModel.views.length;

		log(context, "Putting "+dataNodes.length+" objects in "+childModel.id+", with "+childModel.views.length+" views");
		//alert( "Putting new data in "+childModel.id);
	
	
		// Now put the data into the childModel so that it will refresh.
        childModel.putData(resultNode, true);
				
		views = childModel.views.length;		

		//var modelData = cordys.getXML(childModel.getData());

		log(context, "Finished calling putData. Views registered: "+childModel.views.length);

		// Debug statements
//                log(context, "Put data: "+xmlInChild );
       //         log(context, "Model data: "+modelData);

	}
	else
	{
		log(context, "Clearing data in "+childModel.id);
		childModel.clear();
		delete context.activeBusinessObject;
		
		log(context, "Handled load during "+eventObject.type+" in model "+context.childModel.id+", no data found - putData was not invoked");
		
	}
	indent--;

    context.control.raiseEvent("ondatafilled");

};



modellinker.detachType = function ( control )
{
//	alert("Deleting "+control.linker);
	var context = control.linker;
	delete context.childModel.parentLinker;
	control.linker = 0;
}

/*
 * Below some debug functionality for logging.
 */
var indent = -1;
var logBuffer = "\n";

function log(context, msg)
{
	return;
	for (var i=0; i<indent; i++)
	{
	    logBuffer += "   ";
	}
	
	if (!context)
	{
		logBuffer += msg +"\n";
		return;
	}
	
	logBuffer += context.childModel.id+": ";
	
	var currentBO = " - ";
	if (context.activeBusinessObject)
	{
	    currentBO = context.activeBusinessObject.tagName + "-"+ cordys.getNodeText(context.activeBusinessObject, "tns:Id", " ? ");
	}
	
	var newBO = " - ";
	if (context.parentModel.activeBusinessObject)
	{
	    newBO = context.parentModel.activeBusinessObject.tagName + "-"+ cordys.getNodeText(context.parentModel.activeBusinessObject, "tns:Id", " ? ");
	}
	logBuffer += msg + ", current BO: "+currentBO+", new BO: "+newBO+"\n";
//	alert(msg);
}

function showLogCallback(result)
{
	if (result==1)
	{
		logBuffer = "\n";
	}
}

modellinker.prototype.show = function()
{
	var msg = logBuffer;
	application.confirm(msg, false, showLogCallback, true, "Press yes to clear log buffer");
//	alert(logBuffer);
//	if (!confirm("Press cancel to clear logbuffer"))
	{
//		logBuffer = "";
	}
}


</script>
</head><body></body></html>